import selfissued

import logging

import psycopg2

information_card_object_tag = '''
<OBJECT type="application/x-informationCard"
        name="xmlToken"
        class="skip">
  <PARAM Name="tokenType"
         Value="urn:oasis:names:tc:SAML:1.0:assertion">
  <PARAM Name="issuer"
         Value="http://schemas.xmlsoap.org/ws/2005/05/identity/issuer/self">
  <PARAM Name="requiredClaims"
         Value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier">
  <PARAM Name="optionalClaims"
         Value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
</OBJECT>
'''

class InfocardData(object):
    def __init__(self, revoked=None, user_id=None, alias=None, first_seen=None,
                 personal_identifier=None, rsa_modulus_sha1=None, id=None):
        self.id = id
        self.user_id = user_id
        self.alias = alias
        self.personal_identifier = personal_identifier
        self.rsa_modulus_sha1 = rsa_modulus_sha1
        self.first_seen = first_seen
        self.revoked = revoked

    def isRevoked(self):
        return self.revoked is not None

    @classmethod
    def fromRecord(cls, record):
        return cls(**record)

    @classmethod
    def fromRecordSet(cls, records):
        return map(cls.fromRecord, records)

    @classmethod
    def fromSingletonRecordSet(cls, records):
        try:
            (record,) = records
        except ValueError:
            return None
        else:
            return cls.fromRecord(record)

class SelfissuedSignIn(object):
    def __init__(self, db, key_file=None, common_name=None):
        self.db = db
        self.processor = selfissued.TokenProcessor(key_file,
                                                   common_name=common_name)

    def processXML(self, encrypted_token):
        """Process an encrypted Information Card token as submitted to
        a Web form.

        @returns: The decrypted, parsed token information and the
            MyOpenID information associated with that Information Card
            (associated user, first time seeing it, when revoked)
        @rtype: (selfissued.TokenInformation, InfocardData)
        """
        token = self.processor.processXML(encrypted_token)

        # Get the authentication credentials from the token
        personal_identifier = token.getStandardClaim(
            'privatepersonalidentifier')
        if personal_identifier is None:
            raise selfissued.TokenError('Missing Private Personal Identifier')

        rsa_modulus_sha1 = token.getRSAModulusSHA1()

        data = self.getInfocardDataByAuth(
            rsa_modulus_sha1, personal_identifier)

        # XXX: check the assertion ID to avoid replays
        return token, data

    def getInfocardDataByAuth(self, rsa_modulus_sha1, personal_identifier):
        """Get the infocard data for the credentials that have been presented.

        @returns: The stored data or None if this card has not yet
            been stored in the database.
        @rtype: InfocardData or NoneType
        """
        results = self.db._query("""
        SELECT *
          FROM users_infocards
          WHERE rsa_modulus_sha1 = %s AND personal_identifier = %s
           AND revoked IS NULL
        """, [buffer(rsa_modulus_sha1), personal_identifier])
        return InfocardData.fromSingletonRecordSet(results)

    def getInfocardDataByUser(self, user_id):
        """Get the infocard records for a user

        @returns: A list of stored infocard data for a user
        @rtype: list of InfocardData
        """
        results = self.db._query("""
        SELECT *
          FROM users_infocards
          WHERE user_id = %s
        """, [user_id])
        return InfocardData.fromRecordSet(results)

    def revokeInfocardById(self, infocard_id):
        """Revoke an infocard
        """
        count = self.db._query("UPDATE users_infocards"
                               " SET revoked = now()"
                               " WHERE id = %s", [infocard_id],
                               fetch=False)
        if not count:
            raise IndexError(infocard_id)

    def createInfocardData(self, token, user_id, alias):
        """Create a new Information Card/user association

        @param alias: The user-assigned name for this information
            card. It may be None.

        @type token: selfissued.TokenInformation

        @returns: the InfocardData object that represents this relationship.
        """
        rsa_modulus_sha1 = token.getRSAModulusSHA1()
        personal_identifier = token.getStandardClaim(
            'privatepersonalidentifier')

        try:
            self.db._query("""
        INSERT INTO users_infocards (
          user_id,
          rsa_modulus_sha1,
          personal_identifier,
          first_seen,
          alias
        )
        VALUES (%s, %s, %s, now(), %s)
        """, [user_id, buffer(rsa_modulus_sha1), personal_identifier, alias],
                           fetch=False)
        except psycopg2.IntegrityError, e:
            logging.exception("IntegrityError")
            raise KeyError(token)

        # Could do this by being explicit about the id, but this is
        # simpler, since we have to extract these fields and be able
        # to look up by them anyway.
        return self.getInfocardDataByAuth(
            rsa_modulus_sha1, personal_identifier)

    def isAssertionIdNew(self, assertion_id, save_until):
        """Return whether this assertion id has been seen before
        during its lifetime. This method will return True once for
        each assertion id during its lifetime. The assertion ids will
        be forgotten after its lifetime is over.

        These checks will ensure that each token is used once (since
        we remember the assertion id until the time that the assertion
        would be expired).

        @param assertion_id: The assertion id from the SAML token that
            was used for infocard sign-in.
        @param save_until: The NotOnOrAfter timestamp in the token
        @type save_until: datetime.datetime

        @returns: Whether this token has already been used during its
            lifetime.
        @rtype: bool
        """
        try:
            self.db._query("""
            INSERT INTO infocard_assertion_ids
            VALUES (%s, %s)
            """, [assertion_id, save_until], fetch=False)
        except psycopg2.IntegrityError, why:
            print why
            # Assertion ID already present
            return False
        else:
            return True
